# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

<#
.SYNOPSIS
Auto-generates the C/C++ header for the JSON glTF schema as a pre-build step.
Refer to https://github.com/KhronosGroup/glTF/tree/2.0/specification/2.0/schema

.DESCRIPTION
Uses the JSON schemas defined in the JSON files in $SCHEMAS_PATH
to auto-generate the C/C++ header file for use in the glTFSDK.
NOTE: intended to be used as a pre-build step in Visual Studio/MSBuild
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory=$True)]
    [string]$outPath
)

# Script should stop immediately on an error
$ErrorActionPreference = "Stop"

if(-not (Test-Path $outPath))
{
    New-Item -Path $outPath -ItemType Directory | Out-Null
}

# Header file name and path
$HEADER_FILE_NAME = "SchemaJson.h"
$HEADER_FILE = Join-Path -Path "$outPath" -ChildPath "$HEADER_FILE_NAME"

# Schema directory path
$SCHEMAS_PATH = ".\schema"

# Working directory
$WORKING_DIR = "..\GLTFSDK"

# Textual constants for use in the header file
$COPYRIGHT_NOTICE =
"// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License."

$AUTOGENERATED_CODE_WARNING =
"// WARNING: This header file was automatically generated
// by $(Split-Path $PSCommandPath -Leaf).
// Modifying this code by hand is not recommended."`

$HEADER_GUARD = "#pragma once"
$MS_NAMESPACE = "namespace Microsoft"
$GLTF_NAMESPACE = "namespace glTF"

$CLASS_NAME = "SchemaJson"

$INCLUDE_LIST = @("<string>", "<unordered_map>")

# Keep track of the indentation level for writing text to the file.
# Increment by ones.
[uint32]$globalIndentLevel = 0

# Keep track of the open scopes in the file, for integrity purposes.
# Does not support opening nested scopes with the same name.
$scopeSet = New-Object System.Collections.Generic.HashSet[String]

# Returns a string of 2*$indentLevel spaces, for use in indenting.
function GetIndent([uint32]$indentLevel)
{
    return " " * (2 * $indentLevel)
}

function DeleteHeader()
{
    Remove-Item -Force -ErrorAction SilentlyContinue $HEADER_FILE
}

# Appends to the header file with the specified indent level.
function AppendToHeader($val, $indentLevel)
{
    $indent = GetIndent $indentLevel
    ("{0}{1}`r`n" -f $indent,$val) | Add-Content $HEADER_FILE
}

# Opens a new scope with the given name. Does not support opening
# nested scopes with the same name.
function OpenScope($name, $scopeSet)
{
    if($scopeSet.Contains("$name"))
    {
        DeleteHeader
        throw "Scope with name $name is already open! " +
            "Nested scopes with the same name are not supported."
    }

    $scopeSet.Add("$name") | Out-Null
    AppendToHeader "$name {" $script:globalIndentLevel
    $script:globalIndentLevel += 1
}

# Closes the scope with the given name. Throws exception if no matching scope is found.
function CloseScope($name, $scopeSet)
{
    if(-not $scopeSet.Contains("$name"))
    {
        DeleteHeader
        throw "Attempting to close scope with name $name that does not exist! " +
            "Nested scopes with the same name are not supported."
    }

    $scopeSet.Remove("$name") | Out-Null

    $script:globalIndentLevel -= 1
    AppendToHeader "}; // end $name scope" $script:globalIndentLevel
}

# Adds the public class modifier
function AddPublicModifier()
{
   AppendToHeader "public:" $Script:globalIndentLevel
}

# Adds all files in $includeList as #includes in the file.
function AddIncludes($includeList)
{
    foreach($item in $includeList)
    {
        AppendToHeader "#include $item" 0
    }
}

# Wraps the argument in C/C++ raw string tags,
# i.e. returns "R(text)"
function WrapInRawStringTag($text)
{
    return "R`"rawstring($($text))rawstring`""
}

# Returns a string defining a const char* const variable with the
# given name and value in the given scope.
# NOTE: $varValue must contain any desired quoting.
function DefineStaticConstPtrToConstChar($varScope, $varName, $varValue)
{
    $declaration = 'const char* const {0}::{1} = {2};' -f $varScope,$varName,$varValue

    return $declaration
}

# Returns a string declaring a static const char* variable (no value).
function DeclareStaticConstPtrToConstChar($varName)
{
    $declaration = 'static const char* const {0};' -f $varName

    return $declaration
}

# Declares the GLTF_SCHEMA_MAP and ROOT_GLTF_SCHEMA variables.
function DeclareGltfSchemaVars()
{
    # Manually handle indenting here
    $indent = GetIndent $script:globalIndentLevel

    $mapvar = "static const std::unordered_map<std::string, std::string> GLTF_SCHEMA_MAP;"

    AppendToHeader $mapvar $script:globalIndentLevel
}

# Defines the GLTF_SCHEMA_MAP variable
# in the $varScope scope,
# using the values in the array of $names as the key and
# those values with the . changed to _ as the values.
function DefineGltfSchemaVars($varScope, [HashTable]$names)
{
    # Manually handle indenting here
    $indent = GetIndent($script:globalIndentLevel)

    $mapvar =
@"
const std::unordered_map<std::string, std::string> $($varScope)::GLTF_SCHEMA_MAP =
$($indent){
$($indent)
"@

    $i = $names.Count -1
    foreach($key in $names.Keys)
    {
        $punctuation = if($i -ne 0) {","} else {" };"}
        $newName = $key -replace "\.","_"
        $mapvar  = $mapvar + "    { `"$($key)`", $newName }$($punctuation)`r`n$($indent)"
        $i -= 1
    }

    AppendToHeader $mapvar $script:globalIndentLevel
}

Write-Host "Beginning generation of SchemaJson.h..." -ForegroundColor Magenta

pushd $WORKING_DIR

DeleteHeader

AppendToHeader $COPYRIGHT_NOTICE $globalIndentLevel
AppendToHeader $AUTOGENERATED_CODE_WARNING $globalIndentLevel
AppendToHeader $HEADER_GUARD $globalIndentLevel

AddIncludes $INCLUDE_LIST

OpenScope $MS_NAMESPACE $scopeSet
OpenScope $GLTF_NAMESPACE $scopeSet

OpenScope "class $CLASS_NAME" $scopeSet
AddPublicModifier

# Create all the constant strings
# Need to keep the basenames of the files for the GLTF_SCHEMA_MAP variable, later
$mapNamesDict = @{}
Get-ChildItem -File -Recurse -Filter "*.json" "$SCHEMAS_PATH" | Sort-Object | ForEach-Object {

   # Get the file content and apply indenting for nice formatting
   $content = ($([system.io.file]::ReadAllText($_.FullName)).Trim('"',"'")) `
      -replace "`n","`n$(GetIndent($Script:globalIndentLevel + 1))" `
      -replace "^","$(GetIndent($Script:globalIndentLevel + 1))"

   $mapNamesDict.Add($_.Name, $content)
   $varName = $_.Name -replace "\.","_"
   $varText = DeclareStaticConstPtrToConstChar $varName
   AppendToHeader $varText $globalIndentLevel
}

DeclareGltfSchemaVars $CLASS_NAME $mapNamesList

CloseScope "class $CLASS_NAME" $scopeSet

# Define all the variables from earlier
foreach($item in $mapNamesDict.GetEnumerator())
{
    $name = $item.Name -replace "\.","_"
    $content = $item.Value
    $varText = DefineStaticConstPtrToConstChar $CLASS_NAME $name (WrapInRawStringTag("`r`n" + $content))
    AppendToHeader $varText $globalIndentLevel
}

DefineGltfSchemaVars $CLASS_NAME $mapNamesDict

CloseScope $GLTF_NAMESPACE $scopeSet
CloseScope $MS_NAMESPACE $scopeSet

# Make sure all our scopes are closed
if($scopeSet.Count -ne 0)
{
    foreach($name in $scopeSet.GetEnumerator())
    {
        Write-Host ("Scope with name {0} is not closed." -f $name) -ForegroundColor Yellow
    }

    DeleteHeader
    throw "Scope(s) were not closed!"
}

Write-Host "Successfully generated SchemaJson.h." -ForegroundColor Magenta
popd
